// TODO: CODE REVIEW & CLEANUP!
//
// ClientAppSupport.cs
//
// Implements the HomeUX client application framework.
//

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.Xml;
using HomeUX.ClientControlSupport;
using HomeUX.Controls;
using HomeUX.Networking;
using HomeUX.SimulationOnClientSupport;
using HomeUX.Synchronization;
using HomeUX.UiUtilities;
using HomeUX.Utilities;

namespace HomeUX.ClientAppSupport
{

/// <summary>
/// TODO
/// </summary>
///
public class ClientFramework : IDialogSite, IScreenOwner
{

    public static void InitializeSyncClientProvider()
    {
        ISyncClient syncClient;
        if (DesignerProperties.IsInDesignTool)
            syncClient = new NullSyncClient();
        else
            syncClient = new SyncClient();

        ClientGlobals.SyncClientProvider = delegate
        {
            return syncClient;
        };
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// The name of the settings XML file that's stored in isolated storage.
    /// </summary>
    const string SettingsFileName = "Settings.xml";

	/// <summary>
	/// After <r>SetAppSetting</r> is called, it is saved within this many milliseconds.
	/// </summary>
	const int SettingsFileSaveMilliseconds = 30000;

    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>HomeScreenName</r>.
    /// </summary>
    const string HomeScreenNameSettingName = "HomeScreenName";

#if false
    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>IsUserDebugMode</r>.
    /// </summary>
    const string IsUserDebugModeSettingName = "IsUserDebugMode";
#endif

    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>ServerHostName</r>.
    /// </summary>
    const string ServerHostNameSettingName = "ServerHostName";

    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>ServerPortNumber</r>.
    /// </summary>
    const string ServerPortNumberSettingName = "ServerPortNumber";

    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>ServerPassword</r>.
    /// </summary>
    const string ServerPasswordSettingName = "ServerPassword";

    /// <summary>
    /// The name of the application setting that defines the persisted value of
    /// <r>ClientFramework.IsSimulatedServerMode</r>.
    /// </summary>
    const string IsSimulatedServerModeSettingName = "IsSimulatedServerMode";

    /// <summary>
    /// The maximum length of the Back button stack.
    /// </summary>
    const int MaxScreenStackCount = 100;

    /// <summary>
    /// Matches one or more white space characters.
    /// </summary>
	static readonly Regex WhiteSpaceRegex = new Regex(@"\s+");

	/// <summary>
	/// When the connection to the server is lost, reconnection is attempted after this many
	/// milliseconds.
	/// </summary>
	const int RetryConnectionMilliseconds = 1000;

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

	/// <summary>
	/// The screen name (not an application setting name) of the connection settings screen.
	/// </summary>
	public const string ConnectionSettingsScreenName = "ConnectionSettingsScreen";

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    // the single application-side instance of ClientFramework
    static ClientFramework _instance;

    /// <summary>
    /// Holds the value of the <r>HomeScreenName</r> property.
    /// </summary>
    string _homeScreenName;

    /// <summary>
    /// Holds the value of the <r>Screens</r> property.
    /// </summary>
    Dictionary<string, Screen> _screens = new Dictionary<string, Screen>();

    /// <summary>
    /// The stack of active dialogs from back to front.
    /// </summary>
    List<DialogState> _dialogs = new List<DialogState>();

    /// <summary>
    /// The Back button stack.  As <r>GoToScreen</r> is used to navigate to screens,
    /// <r>ScreenState</r> objects are added to this list (and older entries are eventually
    /// removed).  When the Back button is clicked, the last item in this list is removed, and
    /// the previous one is displayed.
    /// </summary>
    List<ScreenState> _screenStack = new List<ScreenState>(MaxScreenStackCount);

    /// <summary>
    /// When <n>true</n>, changing the current selection in <r>ClientFramework.ScreenChooser</r> does not cause
    /// navigation to occur.
    /// </summary>
    bool _suppressNavigation;

    /// <summary>
    /// Holds application settings (see <r>SetAppSetting</r> and <r>GetAppSetting</r>).
    /// </summary>
    Dictionary<string, string> _appSettings = new Dictionary<string, string>();

    bool _pendingLayout;

    bool _pendingSaveAppSettings;

    /// <summary>
    /// The <r>IMessageEndpoint</r> used to talk to a real or simulated HomeUX server.
    /// <n>null</n> if we're not connected to a server and haven't begun connecting (or
    /// reconnecting) yet.
    /// </summary>
    IMessageEndpoint _endpoint;

    /// <summary>
    /// The <r>ISyncClient</r> that's used to synchronize HomeUX state with the server.
    /// Non-<n>null</n> once <r>Current</r> has been set.
    /// </summary>
    ISyncClient _syncClient;
    
    SyncNode _deviceStatusNode;

    TouchMessageBox _connectingDialog;//TODO: is this needed?

    string _connectingDialogStatus; // status currently displayed in the "Connecting" dialog

    DispatcherTimer _retryConnectionTimer; // for retrying connection

    IAlertsDisplay _alertsDisplay;

    /// <summary>
    /// Used to delay display a message for a short time before reloading the application.
    /// </summary>
    DispatcherTimer _reloadTimer;

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Public Properties
    //
    /// <summary>
    /// Gets or sets the single application-wide instance of the <r>ClientFramework</r> class.
    /// </summary>
    /// 
    /// <remarks>
    /// <r>Current</r> may only be set once in an application.
    /// </remarks>
    ///
    public static ClientFramework Current
    {
        set
        {
            if (_instance != null)
                throw new InvalidOperationException("ClientFramework.Instance was already set");
            if (value == null)
                throw new ArgumentNullException("value");
            _instance = value;
            _instance.Initialize();
        }
        get
        {
            if (_instance == null)
                throw new InvalidOperationException("ClientFramework.Instance has not been set yet");
            return _instance;
        }
    }

    /// <summary>
    /// Returns <n>true</n> if <r>Current</r> has been set yet.
    /// </summary>
    public static bool HasInstance
    {
        get
        {
            return (_instance != null);
        }
    }

    /// <summary>
    /// Gets or set the application wide "host kiosk mode" setting.
    /// </summary>
    /// 
    /// <remarks>
    /// <para>
    /// Host kiosk mode is specified by <c>&lt;param name="initParams" value="IsHostKioskMode=true"
    /// /&gt;</c> within the application that (typically a .hta (HTML application) in the case of
    /// <c>IsHostKioskMode=true</c>), indicating that the Silverlight plug-in occupies the full
    /// screen.  This is generally not implemented using
    /// <n>Application.Current.Host.Content.IsFullScreen</n> -- that property is used to implement
    /// "simulated kiosk mode" (in which, for example, pressing Esc exits that mode).
    /// </para>
    /// <para>
    /// Setting the value of <r>IsHostKioskMode</r> also sets the value of <r>ClientGlobals.IsTouchMode</r>, but
    /// the opposite is not true.
    /// </para>
    /// </remarks>
    ///
    public bool IsHostKioskMode { get; private set; }

    public bool IsHideCursorMode { get; set; }

    /// <summary>
    /// Gets or sets the value of the "simulated server mode" setting.
    /// </summary>
    /// 
    /// <remarks>
    /// <para>
    /// In "simulated server mode", the HomeUX server is simulated locally within the Silverlight
    /// client application.
    /// </para>
    /// <para>
    /// <r>DisconnectFromServer</r> must be called before changing the value of this property.
    /// </para>
    /// </remarks>
    ///
    public bool IsSimulatedServerMode { get; set; }

    void SimulateConnection()
    {
        TopLevelPage.Dispatcher.BeginInvoke(delegate
        {
            var simulationEndpoint = _endpoint as SimulationEndpoint;
            if (simulationEndpoint != null)
                simulationEndpoint.FireIsConnectedChanged();
        });
    }

    void InitializeClientEndpoint()
    {
        _endpoint.IsConnectedChanged += _endpoint_IsConnectedChanged;
        _endpoint.MessageReceived += _endpoint_MessageReceived;
        ((SyncClient)_syncClient).Endpoint = _endpoint; // assumes default SyncClient
    }

    /// <summary>
    /// Specifies the status of the connection to the server.
    /// </summary>
    public ConnectionStatus ConnectionStatus { get; private set; }

    /// <summary>
    /// The collection of screens.  Each key is a screen name (as specified by the <n>Tag</n> of
    /// the corresponding item within <n>ClientFramework.ScreenChooser</n>); each value is the corresponding
    /// <r>Screen</r> object.
    /// </summary>
    public IDictionary<string, Screen> Screens
    {
        get
        {
            return new ReadOnlyDictionary<string, Screen>(_screens);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public string HomeScreenName
    {
        get
        {
            return _homeScreenName;
        }
        set
        {
            _homeScreenName = value;
            SetAppSetting(HomeScreenNameSettingName, _homeScreenName);
            UpdateHomeButton();
        }
    }

    // these properties must be set before setting Current, and must not be changed after that...

	/// <summary>
	/// TODO
	/// </summary>
    public StartupEventArgs StartupEventArgs { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public Control TopLevelPage { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public ScaleTransform TopLevelPageTransform { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public Grid TopLevelPageLayoutRoot { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public TouchTabStrip ScreenChooser { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public Grid ScreenArea { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public TouchButton HomeButton { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public TouchButton BackButton { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public string DefaultHomeScreenName { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public Brush DefaultBackgroundBrush { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public ClientStatus ClientStatus { get; set; }
	/// <summary>
	/// TODO
	/// </summary>
    public ServerSimulatorBase ServerSimulator { get; set; }

    /// <summary>
    /// Gets or sets the application-wide auto-repeat delay time used in touch-enabled user
    /// interface elements such as <r>TouchButton</r>.  This delay time is the time from when the
    /// user first touches the user interface element to the time of the first auto-repeat.
    /// </summary>
    ///
    /// <remarks>
    /// This property is only used for user interface elements that have an auto-repeat feature,
    /// and where that feature is enabled.  For example, see
    /// <r>TouchButton.IsAutoRepeatEnabled</r>.
    /// </remarks>
    ///
    /// <seealso cref="AutoRepeatInterval" />
    ///
    public TimeSpan AutoRepeatDelay { get; set; }

    /// <summary>
    /// Gets or sets the application-wide auto-repeat interval time used in touch-enabled user
    /// interface elements such as <r>TouchButton</r>.  This interval is the time between
    /// successive auto-repeats.
    /// </summary>
    ///
    /// <remarks>
    /// This property is only used for user interface elements that have an auto-repeat feature,
    /// and where that feature is enabled.  For example, see
    /// <r>TouchButton.IsAutoRepeatEnabled</r>.
    /// </remarks>
    ///
    /// <seealso cref="AutoRepeatDelay" />
    ///
    public TimeSpan AutoRepeatInterval { get; set; }

    public Screen CurrentScreen
    {
        get
        {
            if (_screenStack.Count > 0)
                return _screenStack[_screenStack.Count - 1].Screen;
            else
                return null;
        }
    }

    /// <summary>
    /// Gets or sets the name of the HomeUX server.  Call <r>DisconnectFromServer</r> before
	/// setting this property.
    /// </summary>
    public string ServerHostName { get; set; }

    /// <summary>
    /// Gets or sets the TCP/IP port number used by the HomeUX server.  Call
	/// <r>DisconnectFromServer</r> before setting this property.
    /// </summary>
    public int ServerPortNumber { get; set; }

    /// <summary>
    /// Gets or sets the password of the HomeUX server.  Call <r>DisconnectFromServer</r> before
	/// setting this property.
    /// </summary>
    public string ServerPassword { get; set; }

    public bool IsInitialized { get; private set; }

    public bool CanReload
    {
        get
        {
            return System.Windows.Browser.HtmlPage.IsEnabled;
        }
    }

    public void Reload()
    {
        // do nothing if reload isn't supported (e.g. in a standalone Silverlight application)
        if (!CanReload)
            return;

        // display a message, and then wait a short time to ensure the message is visible before
        // beginning the reload operation
        TouchMessageBox.Show(TopLevelPage, "Reloading...", null, null);
        _reloadTimer = new DispatcherTimer()
        {
            Interval = TimeSpan.FromMilliseconds(100)
        };
        _reloadTimer.Tick += delegate
        {
            HtmlPage.Window.Eval("location.reload()");
        };
        _reloadTimer.Start();
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Public Methods
    //

    /// <summary>
    /// TODO
    /// </summary>
    ///
    void Initialize()
    {
        // to simplify design-time usage, ClientFramework specifies that "Tag" rather than
        // "Item.Data" should be used to store item values -- see Item for more information
        Item.IsUsingDataProperty = false;

        if (Thread.CurrentThread.Name == null)
            Thread.CurrentThread.Name = "UI";

        // read the application settings XML file (if any)
        LoadAppSettings();

        // parse <param name="initParams" value="..." ...>
        string value;
        StartupEventArgs.InitParams.TryGetValue("ServerHostName", out value);
        ServerHostName = value;
        IsHostKioskMode = StartupEventArgs.InitParams.TryGetValue("IsHostKioskMode", out value) &&
            (value == "true");
        if (IsHostKioskMode)
            ClientGlobals.IsTouchMode = true;
        IsHideCursorMode = StartupEventArgs.InitParams.TryGetValue("IsHideCursorMode", out value) &&
            (value == "true");
        if (StartupEventArgs.InitParams.TryGetValue("IsSimulatedServerMode", out value))
            IsSimulatedServerMode = (value == "true");
        else
        {
            IsSimulatedServerMode =
                XmlConvert.ToBoolean(GetAppSetting(IsSimulatedServerModeSettingName, "false"));
        }

        TopLevelPageLayoutRoot.Background = new SolidColorBrush(
            Application.Current.IsRunningOutOfBrowser ? Colors.Gray :
                Application.Current.Host.Background);


        TopLevelPageLayoutRoot.KeyDown += new KeyEventHandler(LayoutRoot_KeyDown);

		// if requested, hide the cursor -- we do this instead of requiring that the host
		// provide this functionality so (a) it works even if the host doesn't have this feature,
		// and (b) in a multi-monitor configuration where one or more monitors are not touch
		// screens we want cursor support on non-touch-screen monitors
        if (IsHideCursorMode)
			TopLevelPage.Cursor = System.Windows.Input.Cursors.None;

        _syncClient = ClientGlobals.GetSyncClient(null); // assumes default ISyncClient
        ((SyncClient)_syncClient).InstanceIdChanged += ClientFramework_InstanceIdChanged;

        _deviceStatusNode = (SyncNode) _syncClient.SyncTree.CreateNode(SyncPath.FromString("DeviceStatus"));
        _deviceStatusNode.DescendantChanged += _deviceStatusNode_DescendantChanged;

        //Page.KeyDown += new KeyEventHandler(Page_KeyDown);

        // initialize ClientGlobals
		InitializeClientGlobals();

        foreach (object obj in ScreenChooser.Items.ToArray())
        {
            var item = obj as FrameworkElement;
            if (item == null)
                continue;

            string screenName, label;
            Item.GetUntypedValueAndLabel(item, out screenName, out label);
            if (screenName == null)
                continue;
            string labelOverride = ScreenTab.GetLabel(item);
            if (labelOverride != null)
                label = labelOverride;
            else
            if (label == null) // e.g. tab is an <Ellipse> instead of a <TextBlock>
                label = screenName;

            // replace white space characters (including newlines etc.) in <label> with space
            // characters; merge multiple consecutive spaces into one; trim leading and trailing
            // white space
            label = WhiteSpaceRegex.Replace(label, " ").Trim();

            // 
            Control screenControl = ScreenTab.GetScreenControl(item);
            if (screenControl != null)
                ScreenTab.SetScreenControl(item, null);

            if (screenControl != null)
            {
                ScreenArea.Children.Add(screenControl);

                Screen screen = new Screen()
                {
                    Owner = this,
                    Name = screenName,
                    DefaultParentName = ScreenTab.GetDefaultParentName(item),
                    DisableDirectNavigation = ScreenTab.GetDisableDirectNavigation(item),
                    Label = label,
                    Control = screenControl,
                    HasTab = (item.Visibility != Visibility.Collapsed),
                    TabItem = item
                };
                screen.InitArgs = new Screen.InitializationArguments(screen, ScreenTab.GetInitArgs(item));
                //screenControl.Loaded += OnScreenLoaded;
                screenControl.SizeChanged += OnScreenSizeChanged;
                _screens.Add(screenName, screen);
            }
            else
                ScreenChooser.Items.Remove(item);

            var alertsDisplay = screenControl as IAlertsDisplay;
            if (alertsDisplay != null)
            {
                _alertsDisplay = alertsDisplay;
                ((SyncClient)_syncClient).AlertsDisplay = _alertsDisplay;
                ClientGlobals.AlertsDisplay = _alertsDisplay;
            }
        }

        // error if there are no screens
        if (_screens.Count == 0)
            throw new InvalidOperationException("No screens defined");

        // add the "please wait" pseudo-screen
        var pleaseWaitScreen = new Grid()
        {
            Background = DefaultBackgroundBrush,
        };
        /*
        pleaseWaitScreen.Children.Add(new TextBlock()
        {
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
            Text = "Please wait...",
            Foreground = new SolidColorBrush(Colors.Gray)
        });
        */
        ScreenArea.Children.Add(pleaseWaitScreen);

        // set <HomeScreenName> to the name of the home screen, as specified by the
        // HomeScreenNameSettingName application setting
        _homeScreenName = GetAppSetting(HomeScreenNameSettingName, DefaultHomeScreenName);
        if (_homeScreenName != null)
        {
            if (TryFindScreen(_homeScreenName) == null)
                _homeScreenName = null;
        }
        if (_homeScreenName == null)
            _homeScreenName = _screens.Values.First().Name;

        Application.Current.Host.Content.FullScreenChanged += delegate
        {
            PerformLayoutAsync();
        };

        HomeButton.Click += delegate
        {
            GoHome();
        };

        BackButton.Click += delegate
        {
            GoBack();
        };

        ScreenChooser.SelectedItemDataChanged += OnTabSelected;
        ScreenChooser.CurrentItemSelected += OnTabSelected;
    }

    void ClientFramework_InstanceIdChanged(object sender, EventArgs e)
    {
        // the server stopped and restarted -- reload this Silverlight application,
        // in case the application itself changed between the stop and restart operations
        Reload();
    }

    /// <summary>
    /// Initializes the state of the static class <r>ClientGlobals</r> so that
    /// <r>ClientGlobals</r> effectively "points to" <r>ClientFramework</r> as the provider of
    /// application-wide services.
    /// </summary>
    ///
	void InitializeClientGlobals()
	{
        ClientGlobals.SyncClientProvider = delegate
        {
            return _syncClient;
        };

        ClientGlobals.TryGoToScreen = TryGoToScreen;
        //ClientGlobals.ScreenControlToScreen = TryFindScreenByControl;
        //ClientGlobals.ScreenNameToScreen = TryFindScreenByName;

        // if the <param name="initParams"> (in the Silverlight plug-in) didn't specify a value for
        // ServerHostName, get the value from isolated storage; if there's no value there, default
        // it to the host name of this document, or "localhost" in the case of F5 debugging
        if (ServerHostName == null)
        {
            ServerHostName = GetAppSetting(ServerHostNameSettingName,
                (Application.Current.IsRunningOutOfBrowser ? String.Empty :
                    HtmlPage.Document.DocumentUri.Host));
            if (ServerHostName.Length == 0)
                ServerHostName = "localhost"; // F5 debugging
        }

        // get ServerPortNumber and ServerPassword from isolated storage; if there's not value
        // there, use defaults -- in the case of ServerPassword, the default is String.Empty,
        // which causes the user to be prompted for the password later
        ServerPortNumber =
            XmlConvert.ToInt32(GetAppSetting(ServerPortNumberSettingName, "4510"));
        ServerPassword = GetAppSetting(ServerPasswordSettingName, String.Empty);

        // set the default dialog site provider
        ClientGlobals.DialogSiteProvider = delegate(FrameworkElement context)
        {
            return this;
        };

        ClientGlobals.AutoRepeatDelay = AutoRepeatDelay;
        ClientGlobals.AutoRepeatInterval = AutoRepeatInterval;

        ClientGlobals.DefaultBackground = DefaultBackgroundBrush;
	}

    TouchMessageBox _simulationStartingMessageBox;

    /// <summary>
    /// Starts connecting to the HomeUX server -- either the real or simulated server, depending
    /// on the value of <r>IsSimulatedServerMode</r>.  Does nothing if we're already connected, or
    /// if the connection process has already begun (e.g. due to a prior call to
    /// <n>BeginConnectingToServer</n>).
    /// </summary>
    ///
    public void BeginConnectingToServer()
    {
        if (_endpoint != null)
            return;

        ClientStatus.IsSimulated = IsSimulatedServerMode;

        if (IsSimulatedServerMode)
        {
            // simulated server mode
            ConnectionStatus = ConnectionStatus.Connected;
            var endpoint = new SimulationEndpoint(TopLevelPage.Dispatcher, ServerSimulator);
            _endpoint = endpoint;
            endpoint.SimulationStarted += OnSimulationStarted;
            _simulationStartingMessageBox = TouchMessageBox.Show(TopLevelPage,
                "The HomeUX server simulator is starting...", null, delegate
            {
                _simulationStartingMessageBox = null;
            });
            endpoint.StartSimulation();
        }
        else
        {
            // real server mode
            ConnectionStatus = ConnectionStatus.IntentionallyDisconnected;
            _endpoint = new ClientEndpoint();
            ((ClientEndpoint)_endpoint).StatusChanged += _endpoint_StatusChanged;
            InitializeClientEndpoint();

            if (NeedConnectionInfo())
            {
                ConnectionStatus = ConnectionStatus.ConnectionSettingsNeeded;
                TryGoToScreen(ConnectionSettingsScreenName, null);
            }
            else
            {
                ConnectionStatus = ConnectionStatus.AttemptingConnection;
                var clientEndpoint = _endpoint as ClientEndpoint;
                if (clientEndpoint == null)
                    return;
                // TODO: this is a race condition just waiting to happen: ClientEndpoint is partially
                // protected from race conditions because:
                //   -- ReportedStatus is based on what Poll knows
                //   -- SendMessage is robustly handles (via false return code) underlying connection drop.
                // BUT, here are some exceptions:
                //   -- ServerHostName, ServerPortNumber, etc. access ClientEndpoint's "immediate" status,
                // not the "reported" status -- so what happens if ClientEndpoint isn't fully disconnected?
                clientEndpoint.ServerHostName = ServerHostName;
                clientEndpoint.ServerPortNumber = ServerPortNumber;
                clientEndpoint.Password = ServerPassword;
                clientEndpoint.ConnectToServer();
                ShowConnectingDialog("Finding " + ServerHostName);
            }
        }
    }

    void OnSimulationStarted(object sender, EventArgs e)
    {
        var endpoint = _endpoint as SimulationEndpoint;
        if (endpoint != null)
            endpoint.SimulationStarted -= OnSimulationStarted;
        if (_simulationStartingMessageBox != null)
            _simulationStartingMessageBox.Close(false);
        InitializeClientEndpoint();
        SimulateConnection();
    }

    /// <summary>
    /// Disconnects from the HomeUX server (real or simulated), or ends the current attempt to
    /// connect that was begun by <r>BeginConnectingToServer</r>.  Does nothing if we're not
    /// currently connected or attempting to connect.
    /// </summary>
	///
    public void DisconnectFromServer()
    {
        if (_endpoint != null)
        {
            if (!IsSimulatedServerMode)
            {
                ((ClientEndpoint)_endpoint).StatusChanged -= _endpoint_StatusChanged;
                ((ClientEndpoint)_endpoint).CloseConnection();
            }
            ((SyncClient)_syncClient).Endpoint = null; // assumes default SyncClient
            _endpoint.IsConnectedChanged -= _endpoint_IsConnectedChanged;
            _endpoint.MessageReceived -= _endpoint_MessageReceived;
            ((IDisposable)_endpoint).Dispose();
            _endpoint = null;

            ((SyncNode)_syncClient.SyncTree).NullifySubtree();

            foreach (Screen screen in _screens.Values)
                screen.FireReset();
        }

        ConnectionStatus = ConnectionStatus.IntentionallyDisconnected;
    }

    void LayoutRoot_KeyDown(object sender, KeyEventArgs e)
    {
        // pressing Backspace does the same as GoBack, unless focus is in a text box
        var focusedElement = FocusManager.GetFocusedElement();
        if (!(focusedElement is TextBox))
        {
            if (e.Key == Key.Back)
            {
                e.Handled = true;
                GoBack();
                return;
            }
			/*
            else
            if (e.Key == Key.Home)
            {
                e.Handled = true;
                GoHome();
                return;
            }
			*/
        }
    }

    void _deviceStatusNode_DescendantChanged(object sender, DescendantChangedEventArgs e)
    {
        if ((e.Changes & DescendantChanges.DeepNodeValueChanged) != 0)
        {
            SyncPath busyCountPath = SyncPath.FromString("BusyCount");
            int totalBusyCount = 0;
            foreach (SyncNodeBase deviceNode in _deviceStatusNode.Children)
                totalBusyCount += deviceNode.TryFindValue(busyCountPath, 0);
            ClientStatus.IsBusy = (totalBusyCount != 0);
        }
    }

    /// <summary>
    /// Saves connection settings.  Optionally saves the password, or erases the saved password.
    /// </summary>
    /// <param name="savePassword"></param>
    public void SaveConnectionSettings(bool savePassword)
    {
        SetAppSetting(ServerHostNameSettingName, ServerHostName);
        SetAppSetting(ServerPortNumberSettingName,
            XmlConvert.ToString(ServerPortNumber));
        SetAppSetting(ServerPasswordSettingName,
            (savePassword ? ServerPassword : String.Empty));
        SetAppSetting(IsSimulatedServerModeSettingName,
            XmlConvert.ToString(IsSimulatedServerMode));
    }

    public bool IsPasswordSaved
    {
        get
        {
            return !String.IsNullOrEmpty(GetAppSetting(ServerPasswordSettingName, null));
        }
    }

    bool _allScreensAreLoaded = false;

    void OnScreenSizeChanged(object sender, EventArgs e)
    {
        if (_allScreensAreLoaded)
            return;

        bool anyScreenNotLoadedYet = false;
        foreach (Screen screen in _screens.Values)
        {
            if (object.ReferenceEquals(screen.Control, sender))
                screen.IsLoaded = true;
            else
            if (!screen.IsLoaded)
                anyScreenNotLoadedYet = true;
        }
        if (!anyScreenNotLoadedYet)
            OnAllScreensLoaded();
    }

#if false
    void OnScreenLoaded(object sender, EventArgs e)
    {
        //bool anyScreenNotLoadedYet = false;
        foreach (Screen screen in _screens.Values)
        {
            if (object.ReferenceEquals(screen.Control, sender))
            {
                var ctl = screen.Control as ConnectionSettingsScreen;
                if (ctl != null)
                {
                    int i = 1;
                }
                //screen.IsLoaded = true;
                screen.IsAlmostLoaded = true;
            }
            //else
            //if (!screen.IsLoaded)
            //    anyScreenNotLoadedYet = true;
        }
        //if (!anyScreenNotLoadedYet)
        //    OnAllScreensLoaded();
    }
#endif

    void OnAllScreensLoaded()
    {
        PerformLayout();
        IsInitialized = true;
        GoHome();
        BeginConnectingToServer();
    }

    bool NeedConnectionInfo()
    {
        return String.IsNullOrEmpty(ServerHostName) ||
            (ServerPortNumber <= 0) ||
            String.IsNullOrEmpty(ServerPassword);
    }

    /// <summary>
    /// Performs actions that should be performed when the application exits.
    /// </summary>
    public void OnExit()
    {
        DisconnectFromServer();
        SaveAppSettings();
    }

    // for ClientGlobals
    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="screenName">TODO</param>
    ///
    bool TryGoToScreen(string screenName)
    {
        return TryGoToScreen(screenName, null);
    }

    public bool TryFindScreen<T>(string screenName, out Screen screen, out T screenControl)
        where T : Control
    {
        screen = TryFindScreen(screenName);
        if (screen == null)
        {
            screenControl = null;
            return false;
        }

        screenControl = screen.Control as T;
        if (screenControl == null)
        {
            throw new ArgumentException(String.Format(
                "Screen \"{0}\" is not of type {1}",
                screen.Name, typeof(T).Name));
        }

        return true;
    }

    public void FindScreen<T>(string screenName, out Screen screen, out T screenControl)
        where T : Control
    {
        if (!TryFindScreen(screenName, out screen, out screenControl))
            throw new ArgumentException(String.Format("Screen \"{0}\" was not found", screenName));
    }

    /// <summary>
    /// Navigates to a screen with a given name.  Does nothing if the given screen name is
    /// <n>null</n> or doesn't exist.
    /// </summary>
    ///
    /// <param name="screenName">The name of the screen to navigate to.</param>
    ///
    /// <param name="parentScreenName">If the screen specified by <pr>screenName</pr> doesn't have a
    /// 	tab in the tab strip, it's shown to be a child of the screen named
    /// 	<pr>parentScreenName</pr> (if <pr>parentScreenName</pr> is not <n>null</n>), otherwise it's
    /// 	shown to be unrelated to any tab.</param>
    ///
    /// <returns>
    /// <n>true</n> if a screen named <pr>screenName</pr> exists, <n>false</n> if not.  Note that
    /// there are circumstances where navigation will fail even though the specified screen
    /// exists -- in these cases, <n>true</n> is still returned.
    /// </returns>
    public bool TryGoToScreen(string screenName, string parentScreenName)
    {
        Screen screen;
        if ((screenName != null) && Screens.TryGetValue(screenName, out screen))
        {
            GoToScreen(screen, parentScreenName, null);
            return true;
        }
        else
            return false;
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <param name="screenName">TODO</param>
    ///
    /// <param name="parentScreenName">TODO</param>
    ///
    public void GoToScreen(string screenName, string parentScreenName)
    {
        if (!TryGoToScreen(screenName, parentScreenName))
            throw new ArgumentException(String.Format("Screen \"{0}\" was not found", screenName));
    }

    /// <summary>
    /// Navigates to a screen, given the type of the control that implements the
    /// screen.  Returns <n>false</n> if the screen is not found, or if there is more than one
    /// screen of that type.
    /// </summary>
    ///
    /// <param name="screenType">The type of the control that implements the screen.</param>
    /// 
    /// <param name="parentScreenType">The type of the control that implements the parent screen, or
    ///     <n>null</n> to specify no parent screen.</param>
    ///
    /// <returns>
    /// <n>true</n> if a screen of type <pr>screenType</pr> exists, <n>false</n> if not.  Note that
    /// there are circumstances where navigation will fail even though the specified screen
    /// exists -- in these cases, <n>true</n> is still returned.
    /// </returns>
    ///
    /// <remarks>
    /// <para>
    /// Using this override of <n>TryGoToScreen</n>, rather than one which specifies the
    /// destination by screen name, helps find errors at compile time rather than run time.
    /// </para>
    /// <para>
    /// It's permissible to have more than one screen with the same screen type (i.e. the same
    /// type of control that implements the screen), but in that case you must navigate to the
    /// screen using its name rather than its type.
    /// </para>
    /// </remarks>
    ///
    public bool TryGoToScreen(Type screenType, Type parentScreenType)
    {
        Screen screen = TryFindScreen(screenType);
        if (screen != null)
        {
            string parentScreenName;
            if (parentScreenType != null)
            {
                Screen parentScreen = TryFindScreen(parentScreenType);
                parentScreenName = (parentScreen != null) ? parentScreen.Name : null;
            }
            else
                parentScreenName = null;
            GoToScreen(screen, parentScreenName, null);
            return true;
        }
        else
            return false;
    }

    public void GoToScreen(Type screenType, Type parentScreenType)
    {
        if (!TryGoToScreen(screenType, parentScreenType))
        {
            throw new ArgumentException(String.Format("Screen type \"{0}\" was not found",
				screenType.Name));
        }
    }

    /// <summary>
    /// Returns the <n>Screen</n> for a screen, given the name of the screen (i.e. its
    /// <n>Tag</n>).  Returns <n>null</n> if the screen is not found.
    /// </summary>
    ///
    /// <param name="screenName">The name of the screen to find.</param>
    ///
    public Screen TryFindScreen(string screenName)
    {
        Screen screen;
        if (Screens.TryGetValue(screenName, out screen))
            return screen;
        else
            return null;
    }

    /// <summary>
    /// Returns the <n>Screen</n> for a screen, given the type of the control that implements the
    /// screen.  Returns <n>null</n> if the screen is not found, or if there is more than one
    /// screen of that type.
    /// </summary>
    ///
    /// <param name="screenType">The type of the control that implements the screen.</param>
    ///
    /// <remarks>
    /// It's permissible to have more than one screen with the same screen type (i.e. the same
    /// type of control that implements the screen), but in that case you must use
    /// <r>TryFindScreen</r><b>(String)</b> instead of <r>TryFindScreen</r><b>(Type)</b>.
    /// </remarks>
    ///
    public Screen TryFindScreen(Type screenType)
    {
        return Screens.Values.SingleOrDefault(s => s.Control.GetType() == screenType);
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    /// <typeparam name="T">TODO</typeparam>
    ///
    public T TryFindScreenControl<T>() where T : Control
    {
        Screen screen = TryFindScreen(typeof(T));
        if (screen == null)
            return null;
        return (T) screen.Control;
    }

    /// <summary>
    /// Returns the <n>Screen</n> for a screen, given the instance of the control that implements
    /// the screen.  Returns <n>null</n> if the screen is not found.
    /// </summary>
    ///
    /// <param name="screenControl">The type of the control that implements the screen.</param>
    ///
    public Screen TryFindScreen(Control screenControl)
    {
        return Screens.Values.SingleOrDefault(s => s.Control == screenControl);
    }

    /// <summary>
    /// Sets an application setting, which will get saved to isolated storage within a short time
    /// or when <r>SaveAppSettings</r> is called, whichever happens first.
    /// </summary>
    ///
    /// <param name="name">The name of the setting.</param>
    ///
    /// <param name="value">The value of the setting.</param>
    ///
    public void SetAppSetting(string name, string value)
    {
        _appSettings[name] = value;
        SaveAppSettingsAsync();
    }

    /// <summary>
    /// Gets an application setting.  Settings are stored in isolated storage.
    /// </summary>
    ///
    /// <param name="name">The name of the setting.</param>
    ///
    /// <param name="defaultValue">The value that is returned if the specified setting doesn't
    ///     exist.</param>
    ///
    public string GetAppSetting(string name, string defaultValue)
    {
        string value;
        if (_appSettings.TryGetValue(name, out value))
            return value;
        else
            return defaultValue;
    }

    /// <summary>
    /// Saves application settings (i.e. settings set using <r>SetAppSetting</r>) to isolated
    /// storage.  These settings will be reloaded the next time the application
    /// loads.
    /// </summary>
    public void SaveAppSettings()
    {
        _pendingSaveAppSettings = false;

        using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForSite())
        {
            using (IsolatedStorageFileStream stream = store.CreateFile(SettingsFileName))
            {
                XmlWriterSettings writerSettings = new XmlWriterSettings()
                {
                    Indent = true
                };
                using (XmlWriter writer = XmlWriter.Create(stream, writerSettings))
                {
                    writer.WriteStartElement("Settings");
                    foreach (KeyValuePair<string, string> kvp in _appSettings)
                    {
                        writer.WriteStartElement("Setting");
                        writer.WriteAttributeString("Name", kvp.Key);
                        writer.WriteAttributeString("Value", kvp.Value);
                        writer.WriteEndElement();
                    }
                    writer.WriteEndElement();
                }
            }
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Virtual Methods
    //

    public void PerformLayoutAsync()
    {
        if (_pendingLayout)
            return;
        TopLevelPage.Dispatcher.BeginInvoke(PerformLayout);
        _pendingLayout = true;
    }

    public bool AddAlertMessage(AlertType alertType, string message)
    {
        if (_alertsDisplay != null)
        {
            _alertsDisplay.AddMessage(alertType, message);
            return true;
        }
        else
            return false;
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Protected Methods
    //

    protected virtual void PerformLayout()
	{
        _pendingLayout = false;

		// adjust the scale of the page as needed: in full-screen mode, <LayoutRoot> should be as
		// large as possible while still maintaining its aspect ratio and not exceeding the bounds
		// of the screen, and it should be centered
		var x1 = TopLevelPage.ActualWidth;
		var y1 = TopLevelPage.ActualHeight;
		var x2 = TopLevelPageLayoutRoot.Width;
		var y2 = TopLevelPageLayoutRoot.Height;
		double scale;
		Thickness margin;
        if (IsHostKioskMode ||
            Application.Current.Host.Content.IsFullScreen/*simulated kiosk mode*/)
        {
            var scaleX = x1 / x2;
            var scaleY = y1 / y2;
            double extraX = x1 - x2 * scaleY;
            double extraY = y1 - y2 * scaleY;
            if (extraX > 0)
            {
                scale = scaleY;
                margin = new Thickness(extraX / 2, 0, 0, 0);
            }
            else
            {
                scale = scaleX;
                margin = new Thickness(0, extraY / 2, 0, 0);
            }
        }
        else
        {
            scale = 1;
            margin = new Thickness();
        }
		TopLevelPageTransform.ScaleX = scale;
		TopLevelPageTransform.ScaleY = scale;
		TopLevelPageLayoutRoot.Margin = margin;
	}

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Methods
    //

    /// <summary>
    /// Causes <r>SaveAppSettings</r> to be called within <r>SettingsFileSaveMilliseconds</r>
    /// milliseconds.
    /// </summary>
    ///
    void SaveAppSettingsAsync()
    {
        if (_pendingSaveAppSettings)
            return;
        ClientUtil.InvokeOnUiThread(SettingsFileSaveMilliseconds, SaveAppSettings);
        _pendingSaveAppSettings = true;
    }

    /// <summary>
    /// Reads the application settings XML file (stored in isolated storage) and populates
    /// <r>_appSettings</r> using its contents.
    /// </summary>
    ///
    void LoadAppSettings()
    {
        try
        {
            using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForSite())
            {
                using (IsolatedStorageFileStream stream = store.OpenFile(SettingsFileName, System.IO.FileMode.Open, System.IO.FileAccess.Read))
                {
                    _appSettings.Clear();
                    using (XmlReader reader = XmlReader.Create(stream))
                    {
                        // descend into the "Settings" element
                        reader.MoveToContent();
                        if (reader.Name != "Settings")
                            return;
                        if (!reader.Read())
                            return;
                        while (reader.Depth == 1)
                        {
                            reader.MoveToContent();
                            if (reader.Depth != 1)
                                break;
                            if ((reader.NodeType == XmlNodeType.Element) &&
                                (reader.Name == "Setting"))
                            {
                                string name = reader.GetAttribute("Name");
                                string value = reader.GetAttribute("Value");
                                _appSettings[name] = value;
                            }
                            reader.Skip();
                        }
                    }
                }
            }
        }
        catch (Exception ex)
        {
            // of any I/O error occurs (e.g. settings file doesn't exist yet) or any XML error
            // occurs (e.g. malformed XML), just clear <_appSettings> and quit
            if ((ex is IsolatedStorageException) || (ex is XmlException))
                _appSettings.Clear();
            else
                throw;
        }
    }

    /// <summary>
    /// Called when the user selects a new tab or the currently-selected tab, or when
    /// <n>ScreenChooser.SelectedItemData</n> changes in some other way.
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void OnTabSelected(object sender, EventArgs e)
    {
        if (!_suppressNavigation)
            TryGoToScreen(ScreenChooser.SelectedItemData, null);
    }

    /// <summary>
    /// Called when the Silverlight plug-in switches to or from full-screen mode.
    /// </summary>
    ///
    /// <param name="sender">The event sender.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void OnFullScreenChanged(object sender, EventArgs e)
    {
        //ControlHelper.InvokeOnUiThread(5000, delegate() // easier to debug
        PerformLayoutAsync();
        TopLevelPage.Dispatcher.BeginInvoke(() => PerformLayout());
    }

    void GoToScreen(Screen screen, string parentScreenName, ScreenState revertToState)
    {
        // if <parentScreenName> is null, use the default parent screen name that was specified
        // in the XAML declaration of the screens
        if (parentScreenName == null)
            parentScreenName = screen.DefaultParentName;

        // if any dialogs are open, close them
        while (_dialogs.Count > 0)
            ClientUtil.CloseDialog(_dialogs[0].Dialog, false);

        if (((ConnectionStatus == ConnectionStatus.ConnectionSettingsNeeded) ||
             (ConnectionStatus == ConnectionStatus.PasswordIsIncorrect)) &&
            (screen.Name != ConnectionSettingsScreenName))
        {
            TryGoToScreen(ConnectionSettingsScreenName, null);
            return;
        }

        // do the following if <screen> isn't the currently-selected screen
        if ((screen != CurrentScreen) || (revertToState != null))
        {
            // tell the old screen it's closed
            if (CurrentScreen != null)
                CurrentScreen.FireClosed();

            // move <screen> to the top of the z-order within <ScreenArea>; while we're at it,
            // delete the "please wait" pseudo-screen if we find it
            foreach (UIElement element in ScreenArea.Children.ToArray())
            {
                Control thisScreenControl = element as Control;
                if (thisScreenControl != null)
                {
                    // assume <element> is a screen - move it to the top of the z-order if it's the
                    // selected screen
                    bool isSelected = (thisScreenControl == screen.Control);
                    Canvas.SetZIndex(thisScreenControl, (isSelected ? 1 : 0));
                    thisScreenControl.IsEnabled = isSelected;
                }
                else
                {
                    // assume <element> is the "please wait" pseduo-screen -- delete it
                    ScreenArea.Children.Remove(element);
                }
            }

            // set <screenState> to be the state of the screen -- this will be added to the Back
            // history (if <addToStack> is true)
            ScreenState screenState;
            if (revertToState != null)
                screenState = revertToState;
            else
            {
                screenState = new ScreenState()
                {
                    Screen = screen,
                };
                if (screen.HasTab)
                {
                    // <screen> is top-level, i.e. has its own tab
                    screenState.SelectedScreenTag = screen.Name;
                    screenState.IsPartiallySelected = false;
                }
                else
                    if (parentScreenName != null)
                    {
                        // in the tab strip, show that <screen> is a child of screen <parentScreen>
                        screenState.SelectedScreenTag = parentScreenName;
                        screenState.IsPartiallySelected = true;
                    }
                    else
                    {
                        // in the tab strip, show no tab as being selected, i.e. show that <screen> is
                        // not related to any tab
                        screenState.SelectedScreenTag = null;
                        screenState.IsPartiallySelected = false;
                    }
            }

            // update the state of the navigation tab strip
            _suppressNavigation = true;
            try
            {
                ScreenChooser.SelectedItemData = screenState.SelectedScreenTag;
                ScreenChooser.IsPartiallySelected = screenState.IsPartiallySelected;
            }
            finally
            {
                _suppressNavigation = false;
            }

            // clean up the stack, if necessary, and add <screenState> to the Back stack
            if (revertToState == null)
            {
                while (_screenStack.Count >= MaxScreenStackCount)
                    _screenStack.RemoveAt(0);
                _screenStack.Add(screenState);
            }

            // enable the back button if needed
            AdjustBackButton();

            // tell the new screen it's opened
            screen.FireOpened();
            screen.FirePresented();
        }
        else
        {
            // tell the screen that the user touched the same tab again (or equivalent)
            screen.FireRefreshed();
            screen.FirePresented();
        }

        // set focus to the new screen
        screen.Control.Focus();

        // update UI as needed
        UpdateHomeButton();

        if (ConnectionStatus == ConnectionStatus.AttemptingConnection)
            ShowConnectingDialog(null);
    }

    // show "Connecting" dialog; use <status>; if <status> is null, use status from previous call
    void ShowConnectingDialog(string status)
    {
        if (_connectingDialog != null)
        {
            UpdateConnectingDialog(status);
            return;
        }

        string message = FormatConnectingDialog(status);

        string buttons = "Connection Settings...";
        if (CanReload)
            buttons += "|Reload";

        _connectingDialog = TouchMessageBox.Show(TopLevelPage, message,
            buttons,
            delegate(int buttonIndex)
            {
                InitializeConnectingDialogState();
                if (buttonIndex == 0)
                {
                    // "Connection Settings" button
                    DisconnectFromServer();
                    StopRetryConnectionTimer();
                    TryGoToScreen(ConnectionSettingsScreenName, null);
                }
                else
                if (buttonIndex == 1)
                {
                    // "Reload" button
                    DisconnectFromServer();
                    StopRetryConnectionTimer();
                    Reload();
                }
            });
    }

    void UpdateConnectingDialog(string status)
    {
        if (_connectingDialog != null)
            _connectingDialog.Message = FormatConnectingDialog(status);
    }

    string FormatConnectingDialog(string status)
    {
        if (status != null)
            _connectingDialogStatus = status;
        else
            status = _connectingDialogStatus;

        if (status == null) // shouldn't happen
            status = String.Empty;

        return String.Format(
            "Connecting to the server...\n\nStatus: {0}\n\nNote: If the server was started after the client was started, expect a one minute delay before connection will succeed.",
            status);
    }

    void CloseConnectingDialog()
    {
        if (_connectingDialog != null)
        {
            ClientUtil.CloseDialog(_connectingDialog, false);
        }
    }

    void InitializeConnectingDialogState()
    {
        _connectingDialog = null;
    }

    void UpdateHomeButton()
    {
        // highlight the HOme button if we're on the Home screen and there are no dialogs
        // displayed
        HomeButton.IsLit = (CurrentScreen.Name == HomeScreenName) && (_dialogs.Count == 0);
    }

    /// <summary>
    /// Navigates to the home screen.
    /// </summary>
    ///
    void GoHome()
    {
        // do nothing if there is no home screen name
        if (_homeScreenName == null)
            return;

        // navigat to the home screen
        TryGoToScreen(_homeScreenName, null);
    }

    void GoBack()
    {
        if (!CurrentScreen.HasModalMessageBox)
        {
            // if any dialogs are currently being displayed, the fact that focus will be lost
            // by clicking the Back button means that the topmost dialog will be closed, and that's
            // what we want -- don't actually do the Back operation
            if (_dialogs.Count > 0)
                return;
        }

        // set <currentScreenIndex> to the index within <_screenStack> of the screen currently
        // being displayed
        int currentScreenIndex = _screenStack.Count - 1;

        // do nothing if the Back stack contains only the current screen
        if (currentScreenIndex < 1)
            return;

        // discard the ScreenInfo for the current screen, and move <currentScreenIndex> back one
        _screenStack.RemoveAt(currentScreenIndex--);

        // set <screenState> to the state of the previous screen (the one we'll navigate back to)
        ScreenState screenState = _screenStack[currentScreenIndex];

        // navigate to the screen specified by <screenState>
        GoToScreen(screenState.Screen, null, screenState);

        // disable the back button if needed
        AdjustBackButton();
    }

    /// <summary>
    /// Adjusts the display of the back button based on the state of the Back button and dialog
    /// stacks.
    /// </summary>
    ///
    void AdjustBackButton()
    {
        BackButton.IsEnabled = (_screenStack.Count > 1) || (_dialogs.Count > 0);
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // IDialogSite Implementation
    //

    void IDialogSite.ShowDialog(FrameworkElement dialog, DialogSettings settings)
    {
        if (settings == null)
            settings = new DialogSettings();

        Control previousFocus = ((settings.PeekThrough != null) ? settings.PeekThrough :
            FocusManager.GetFocusedElement() as Control);

        // Visual tree will be:
        //   -- Page.LayoutRoot (a Panel)
        //        -- DialogState.Grid
        //             -- DialogState.ContentControl
        //                  -- DialogState.Border
        //                       -- DialogState.Dialog
        //
		// Purpose of each control:
		//
        //   -- DialogState.Border provides a white** border around the dialog, in the case where
		//      there's a peek-through control (otherwise DialogState.Border is basically a no-op).
		//      DialogState.Border also provides a default white background behind the dialog, in
		//      case the dialog doesn't have its own background.
        //        ** Actually the color is ControlHelper.DefaultBrush
		//
		//   -- DialogState.ContentControl exists primarily so that we can set its TabNavigation
		//      property to KeyboardNavigationMode.Cycle.  Once keyboard focus is on a control
		//      inside the dialog, KeyboardNavigationMode.Cycle prevents tabbing to a control
		//      outside the dialog.  DialogState.ContentControl also has a LostFocus event handler:
		//      if focus leaves its subtree for any reason, the dialog will be closed.
		//
		//   -- DialogState.Grid is a "dialog shield" used to cover all controls that should be inaccessible
		//      beneath the dialog: clicking on the grid (above where one of those controls) closes
		//      the dialog.  (If you click a control not covered by the grid, and that control
		//      accepts keyboard focus, then DialogState.ContentControl will cause the dialog to
		//      close, as described above, and then the click action will occur.)  DialogState.Grid
		//      also has a keyboard handler which causes the dialog to close if the Escape key is
		//      pressed.
		//

        ContentControl contentControl = (settings.PeekThrough == null) ? new Shadow() : new ContentControl();
        contentControl.TabNavigation = KeyboardNavigationMode.Cycle;
        contentControl.IsTabStop = false;

        var dialogState = new DialogState()
        {
            Dialog = dialog,
            Border = new Border()
            {
                Child = dialog,
            },
            ContentControl = contentControl,
            Grid = new Grid()
            {
                Width = ScreenArea.ActualWidth,
                Height = ScreenArea.ActualHeight,
                Background = (Brush)Application.Current.Resources["DialogShieldBrush"],
                HorizontalAlignment = HorizontalAlignment.Left,
                VerticalAlignment = VerticalAlignment.Bottom
            },
            PeekThrough = settings.PeekThrough,
            OnClose = settings.OnClose,
            IsModal = settings.IsModal,
            BackgroundMargin = settings.BackgroundMargin,
            PreviousFocus = previousFocus,
            Notify = dialog as INotifyDialog,
        };
        dialogState.ContentControl.Content = dialogState.Border;

        dialogState.Grid.KeyDown += delegate(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Escape)
            {
                e.Handled = true;
                if (!dialogState.IsModal)
                    ClientUtil.CloseDialog(dialog, true);
            }
        };

        dialogState.Grid.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
        //dialogState.Canvas.MouseLeftButtonDown += delegate(object sender, MouseButtonEventArgs e)
        {
            // if user clicked in outside the dialog, close the dialog
            if (e.OriginalSource == sender) // i.e. if event wasn't bubbled from dialog itself
            {
                e.Handled = true;
                if (!dialogState.IsModal)
                    ClientUtil.CloseDialog(dialog, true);
            }
        };

		PositionDialog(dialogState, true);

        int thisDialogIndex = _dialogs.Count;
        _dialogs.Add(dialogState);

        // enable the back button if needed
        AdjustBackButton();

        // close the dialog when focus leaves it; this will be due to a click rather
        // than by tabbing since I restrict tab navigation to controls within the dialog
        dialogState.ContentControl.LostFocus += delegate(object sender, RoutedEventArgs e)
        {
            DependencyObject focus = FocusManager.GetFocusedElement() as DependencyObject;
            for (int dialogIndex = thisDialogIndex; dialogIndex < _dialogs.Count; dialogIndex++)
            {
                DialogState otherDialog = _dialogs[dialogIndex];
                if (IsObjectChildOf(focus, otherDialog.Dialog))
                    return;
            }

            // close the dialog; if this is the last dialog, leave focus on whatever the user
            // set focus to
            ClientUtil.CloseDialog(dialog, (thisDialogIndex > 0));
        };

        TopLevelPage.Dispatcher.BeginInvoke(delegate()
        {
            if (dialogState.Dialog != null)
            {
                var control = dialogState.Dialog as Control;
                if (control != null)
                    control.Focus();
            }
        });

        // update the Home button UI as needed
        UpdateHomeButton();
    }

    void IDialogSite.RemoveGlow(FrameworkElement dialog)
    {
        int unused;
        DialogState dialogState = FindDialog(dialog, out unused);
        if (dialogState == null)
            return;
        RemoveGlow(dialogState);
    }

    void IDialogSite.RepositionDialog(FrameworkElement dialog)
    {
        int unused;
        DialogState dialogState = FindDialog(dialog, out unused);
        if (dialogState == null)
            return;
        PositionDialog(dialogState, false);
    }

    void IDialogSite.CloseDialog(FrameworkElement dialog, bool restorePreviousFocus)
    {
        // close <dialog>, and all dialogs above it in the dialog stack
        int dialogIndex;
        DialogState foundDialogState = FindDialog(dialog, out dialogIndex);
        if (foundDialogState == null)
            return;
        Control previousFocus = null;
        for (dialogIndex = _dialogs.Count - 1; dialogIndex >= 0; dialogIndex--)
        {
            DialogState dialogState = _dialogs[dialogIndex];
            dialogState.ContentControl.Content = null;
            dialogState.Grid.Children.Remove(dialogState.ContentControl);
            TopLevelPageLayoutRoot.Children.Remove(dialogState.Grid);
            //dialogState.Canvas.Children.Remove(dialogState.ContentControl);
            //LayoutRoot.Children.Remove(dialogState.Canvas);
            if (dialogState.Notify != null)
                dialogState.Notify.OnDialogClose();
            _dialogs.RemoveAt(dialogIndex);
            if (Object.ReferenceEquals(dialogState.Dialog, dialog))
            {
                previousFocus = dialogState.PreviousFocus;
                dialogState.Tombstone();
                break;
            }
        }

        dialogIndex--;
        /* re-enable dialog or screen
        if (dialogIndex >= 0)
            _dialogs[dialogIndex].ContentControl.IsEnabled = true;
        else
            ScreenArea.IsEnabled = true;
        */

        if (restorePreviousFocus && (previousFocus != null) && (previousFocus.Parent != null))
            previousFocus.Focus();

        // tell the application that the dialog closed
        if (foundDialogState.OnClose != null)
            foundDialogState.OnClose();

        // disable the back button if needed
        AdjustBackButton();

        // update the Home button UI as needed
        UpdateHomeButton();

        // if there are no dialogs remaining, we're "presenting" the current screen again -- but
        // check asynchronously to see if the current screen has changed (e.g. in the scenario
        // where the user clicks another tab) because in that case we want to avoid firing the
        // Presented event immediately followed by the Closed event
        if (_dialogs.Count == 0)
        {
            Screen currentScreen = CurrentScreen;
            TopLevelPage.Dispatcher.BeginInvoke(delegate
            {
                if (currentScreen == CurrentScreen)
                    CurrentScreen.FirePresented();
            });
        }
    }



    void RemoveGlow(DialogState dialogState)
    {
        if (dialogState.PolygonGlow != null)
        {
            dialogState.Grid.Children.Remove(dialogState.PolygonGlow);
            //dialogState.Canvas.Children.Remove(dialogState.PolygonGlow);
            dialogState.PolygonGlow = null;
        }
    }

	void PositionDialog(DialogState dialogState, bool initialLayout)
	{
        // if ScreenArea has rounded corners, make the "dialog shield" have rounded corners too
        var screenClip = ScreenArea.Clip as RectangleGeometry;
        var dialogShieldClip = new RectangleGeometry()
        {
            Rect = new Rect(1, 0, ScreenArea.ActualWidth - 2, ScreenArea.ActualHeight - 1),
            RadiusX = ((screenClip != null) ? screenClip.RadiusX : 0),
            RadiusY = ((screenClip != null) ? screenClip.RadiusY : 0)
        };

        int margin = (dialogState.BackgroundMargin != 0) ? dialogState.BackgroundMargin : 4;
        if ((dialogState.BackgroundMargin > 0) || (dialogState.PeekThrough != null))
        {
            dialogState.Border.BorderThickness = new Thickness(margin);
            dialogState.Border.Background = ClientGlobals.DefaultBackground;
            dialogState.Border.BorderBrush = dialogState.Border.Background;
        }

        bool dialogIsAbovePeekThroughControl;
        if (dialogState.PeekThrough != null)
        {
            Rect peekThroughControlRect = UiUtil.GetBoundingRect(dialogState.PeekThrough, ScreenArea);

            Rect peekThroughClipRect = UiUtil.InflateRect(peekThroughControlRect, margin, margin, margin, 0);
            dialogState.Grid.Clip = new GeometryGroup()
            {
                FillRule = FillRule.EvenOdd,
                Children = new GeometryCollection()
                {
                    dialogShieldClip,
                    new RectangleGeometry()
                    {
                        Rect = peekThroughClipRect
                    }
                }
            };

            // add <dialog> to the page, and then call UpdateLayout() to synchronously perform layout so we can
            // see its layed-out size
            if (initialLayout)
            {
                dialogState.Grid.Children.Add(dialogState.ContentControl);
                TopLevelPageLayoutRoot.Children.Add(dialogState.Grid);
                //dialogState.Canvas.Children.Add(dialogState.ContentControl);
                //LayoutRoot.Children.Add(dialogState.Canvas);
            }
            dialogState.Border.UpdateLayout();

            // <borderControlRect1> is where the <dialogState.Border> (and
            // <dialogState.ContentControl>) will be if the dialog is positioned at the bottom of the the
            // peek-through control; <overflow1> is the amount, if any, that the dialog would be
            // clipped at the bottom of <ScreenArea> (i.e. <overflow1> will be positive if the
            // dialog is too tall to fit at the bottom of the peek-through control)
            Rect borderControlRect1 = new Rect(peekThroughClipRect.Left,
                peekThroughControlRect.Top + dialogState.PeekThrough.ActualHeight,
                dialogState.Border.ActualWidth, dialogState.Border.ActualHeight);
            double overflow1 = Math.Max(borderControlRect1.Bottom - ScreenArea.ActualHeight, 0);

            // <borderControlRect2> and <overflow2> are the same as <borderControlRect1> and
            // <overflow1>, but they describe the situation where the dialog is positioned
            // at the top of the peek-through control
            Rect borderControlRect2 = new Rect(peekThroughClipRect.Left,
                peekThroughControlRect.Top - dialogState.Border.ActualHeight,
                dialogState.Border.ActualWidth, dialogState.Border.ActualHeight);
            double overflow2 = Math.Max(-borderControlRect2.Top, 0);

            // Vertical positioning algorithm:
            //   1. The first choice is to position the dialog below the peek-through control.
			//   2. If there's not enough spece below the peek-through control, the second choice
			//      is to place the dialog above the peek-through control.
			//   3. However, if there's no space above the peek-through control either, we fall
			//      back to positioning the dialog below the peek-through control and let it clip.
			//
            Rect borderControlRect;
            if ((overflow1 <= 0) || (overflow2 > 0))
            {
                borderControlRect = borderControlRect1;
                dialogIsAbovePeekThroughControl = false;
            }
            else
            {
                borderControlRect = borderControlRect2;
                dialogIsAbovePeekThroughControl = true;
            }

			// Horizontal positioning algorithm:
			//   1. The first choice is to align the left edge of the dialog with the left edge of
			//      peek-through control.
			//   2. If the peek-through control would extend beyond the right edge of ScreenArea,
			//      the second choice is to align the right edge of the peek-through control with
			//      the right edge of ScreenArea.
			//   3. However, if doing #2 would result in the left edge of the dialog extending to
			//      the left edge of the ScreenArea, we alignin the left edge of the dialog with
			//      the left edge of ScreenArea, and let the right side of the dialog clip.
			// Except: one tweak to this algorighm: enough space (<margin> below) is reserved for
            // the PolygonGlow around the control.
            //
            if (borderControlRect.Right + margin > ScreenArea.ActualWidth)
            {
                double maxRight = ScreenArea.ActualWidth - margin;
                double minLeft = margin;
                double left;
                if (borderControlRect.Width <= maxRight)
                    left = maxRight - borderControlRect.Width;
				else
					left = minLeft;
				borderControlRect = new Rect(left, borderControlRect.Top, borderControlRect.Width,
					borderControlRect.Height);
            }

            PointCollection vertices = ClientUtil.RectsToPolygonPoints(new List<Rect>()
            {
                peekThroughClipRect,
                borderControlRect,
            });

            if (!initialLayout)
            {
                // remove old PolygonGlow
				RemoveGlow(dialogState);
            }

            dialogState.PolygonGlow = new PolygonGlow()
            {
                GlowThickness = 10,
                Points = vertices
            };
            dialogState.Grid.Children.Add(dialogState.PolygonGlow);
            //dialogState.Canvas.Children.Add(dialogState.PolygonGlow);

            if (initialLayout)
            {
                dialogState.ContentControl.HorizontalAlignment = HorizontalAlignment.Left;
                dialogState.ContentControl.VerticalAlignment = VerticalAlignment.Top;
            }

            dialogState.ContentControl.Margin = new Thickness(borderControlRect.Left, borderControlRect.Top, 0, 0);
        }
        else
        {
            if (initialLayout)
            {
                dialogState.ContentControl.HorizontalAlignment = HorizontalAlignment.Center;
                dialogState.ContentControl.VerticalAlignment = VerticalAlignment.Center;
                dialogState.Grid.Children.Add(dialogState.ContentControl);
                dialogState.Grid.Clip = dialogShieldClip;
                TopLevelPageLayoutRoot.Children.Add(dialogState.Grid);
                //dialogState.Canvas.Children.Add(dialogState.ContentControl);
                //LayoutRoot.Children.Add(dialogState.Canvas);


            }
            dialogIsAbovePeekThroughControl = false;
        }

        if (dialogState.Notify != null)
            dialogState.Notify.OnDialogPosition(dialogIsAbovePeekThroughControl);
    }

    DialogState FindDialog(FrameworkElement dialog, out int dialogIndex)
    {
        for (dialogIndex = _dialogs.Count - 1; dialogIndex >= 0; dialogIndex--)
        {
            DialogState dialogState = _dialogs[dialogIndex];
            if (!Object.ReferenceEquals(dialogState.Dialog, dialog))
                continue;
            return dialogState;
        }
        return null;
    }

    static bool IsObjectChildOf(DependencyObject control, DependencyObject ancestor)
    {
        while (true)
        {
            if (control == null)
                return false;
            if (Object.ReferenceEquals(control, ancestor))
                return true;
            control = VisualTreeHelper.GetParent(control);
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////////
    // Private Nested Types
    //

    /// <summary>
    /// Information that's stored in <r>_screenStack</r> about a screen that's displayed.
    /// Contains sufficient information to redisplay the screen when the user clicks the Back
    /// button to get back to this <r>ScreenState</r>.
    /// </summary>
    ///
    [DebuggerDisplay("ScreenState {ScreenName}")]
    class ScreenState
    {
        /// <summary>
        /// The screen that's displayed.
        /// </summary>
        public Screen Screen { get; set; }

        /// <summary>
        /// The <n>Tag</n> within <n>ScreenChooser.Items</n> of the tab that's displayed while
        /// this screen is displayed.
        /// </summary>
        public string SelectedScreenTag { get; set; }

        /// <summary>
        /// The value of <n>ScreenChooser.IsPartiallySelected</n> while this screen is displayed.
        /// </summary>
        public bool IsPartiallySelected { get; set; }

        /// <summary>
        /// Gets the name of <r>Screen</r>.
        /// </summary>
        public string ScreenName
        {
            get
            {
                return Screen.Name;
            }
        }
    }

    class DialogState
    {
        public FrameworkElement Dialog;
        public Border Border;
        public ContentControl ContentControl;
        public Grid Grid;
        public Control PeekThrough;
        public PolygonGlow PolygonGlow;
        public Action OnClose;
        public bool IsModal; // if true, clicks on "shield" are ignored
        public int BackgroundMargin;
        public INotifyDialog Notify;

        // PreviousFocus = which control had focus before dialog opened, or null if none
        // note: don't set focus to DialogState.PreviousFocus in all cases, e.g. if user clicks
        // a navbar button -- that would take focus away from that button
        public Control PreviousFocus;

        public void Tombstone()
        {
            Dialog = null;
            ContentControl = null;
            Grid = null;
        }

        public bool IsFocusOnDialog()
        {
            object focus = FocusManager.GetFocusedElement();
            var frameworkElement = focus as FrameworkElement;
            while (frameworkElement != null)
            {
                if (Object.ReferenceEquals(frameworkElement, Dialog))
                    return true;
                frameworkElement = frameworkElement.Parent as FrameworkElement;
            }
            return false;
        }
    }

    void _endpoint_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        // this is just for debugging -- <_syncClient> takes care of interpreting the message
        Util.Trace("Received message: {0} ({1} body bytes)", e.Message.Name,
            e.Message.Body.Length);
    }

    void _endpoint_StatusChanged(object sender, ClientEndpointStatusChangedEventArgs e)
    {
        Util.Trace("** Connection status: {0} **", e.Status);
        UpdateConnectingDialog(e.Status.ToString()); // TODO: proper conversion to string

        switch (e.Status)
        {

        case ClientEndpointStatus.PasswordIncorrect:

            ConnectionStatus = ConnectionStatus.PasswordIsIncorrect;
            TryGoToScreen(ConnectionSettingsScreenName, null);
            break;

        case ClientEndpointStatus.FailedToConnect:
        case ClientEndpointStatus.LostConnection:

            if (ConnectionStatus != ConnectionStatus.IntentionallyDisconnected)
            {
                DisconnectFromServer();
                StartRetryConnectionTimer();
            }
            break;

        }
    }

    // it's a one-shot timer that calls TryToConnect() in RetryConnectionMilliseconds;
    // call StopRetryConnectionTimer() to cancel it
    void StartRetryConnectionTimer()
    {
        StopRetryConnectionTimer();
        if (_retryConnectionTimer == null)
        {
            _retryConnectionTimer = new DispatcherTimer()
            {
                Interval = TimeSpan.FromMilliseconds(RetryConnectionMilliseconds)
            };
            _retryConnectionTimer.Tick += delegate
            {
                _retryConnectionTimer.Stop();
                BeginConnectingToServer();
            };
        }
        _retryConnectionTimer.Start();
    }

    void StopRetryConnectionTimer()
    {
        if (_retryConnectionTimer != null)
            _retryConnectionTimer.Stop();
    }

    void _endpoint_IsConnectedChanged(object sender, EventArgs e)
    {
        // note that SyncClient also receives this event, and will change its online status in
        // response
        if (_endpoint.IsConnected)
        {
            // connected was established...
            
            // update state
            ConnectionStatus = ConnectionStatus.Connected;

            // if the connecting dialog is being displayed, close it
            CloseConnectingDialog();

            // if the connection setting screen is displayed, go to the home screen
            if (CurrentScreen.Name == ConnectionSettingsScreenName)
                GoHome();
        }
        else
        {
            // connection was lost -- reconnection is handled in _endpoint_StatusChanged
        }
    }
}

/// <summary>
/// TODO
/// </summary>
///
public interface IScreenOwner
{
    /// <summary>
    /// TODO
    /// </summary>
    Screen CurrentScreen
    {
        get;
    }
}

/// <summary>
/// Represents a screen that can be displayed in <r>ClientFramework.ScreenArea</r>.
/// </summary>
/// 
/// <remarks>
/// A screen is "declared" by listing it as an item in <r>ClientFramework.ScreenChooser</r>.  If a given
/// item has <c>Visibility="Collapsed"</c>, then the screen has no tab, but it may still be
/// displayed.
/// </remarks>
///
[DebuggerDisplay("Screen {Name}")]
public class Screen
{
    internal IScreenOwner Owner;

    /// <summary>
    /// The name of the screen, as specified by the <n>Tag</n> attribute of the screen's item
    /// within <r>ClientFramework.ScreenChooser</r>.
    /// </summary>
    public string Name { get; internal set; }

    /// <summary>
    /// The name of the screen that's the default parent of this screen, or <n>null</n> if there
    /// is no default parent.
    /// </summary>
    public string DefaultParentName { get; internal set; }

    /// <summary>
    /// If <n>true</n>, navigation to the screen is disabled unless screen-specific parameters are
	/// set up in advance.  So, for example, if <r>DisableDirectNavigation</r> is <n>true</n>,
	/// the screen will not appear in the list of screens that can be specified as the home screen.
    /// </summary>
    public bool DisableDirectNavigation { get; internal set; }

    /// <summary>
    /// A human-readable label for the screen.
    /// </summary>
    public string Label { get; internal set; }

    /// <summary>
    /// The <r>Control</r> that implements the screen.
    /// </summary>
    public Control Control { get; internal set; }

    /// <summary>
    /// Initialization arguments, as specified by the <n>ScreenTab.InitArgs</n> attribute of
    /// the screen's item within <r>ClientFramework.ScreenChooser</r>.
    /// </summary>
    public InitializationArguments InitArgs { get; internal set; }

    /// <summary>
    /// <n>true</n> if the screen is represented by a tab in <r>ClientFramework.ScreenChooser</r>,
    /// <n>false</n> if not.
    /// </summary>
    public bool HasTab { get; internal set; }

    /// <summary>
    /// The <n>FrameworkElement</n> that's contained within the screen's tab, or <n>null</n> if
    /// none.
    /// </summary>
    public FrameworkElement TabItem { get; internal set; }

    TouchMessageBox _modalMessageBox;
    string _modalMessageBoxMessage;
    string _modalMessageBoxButtonLabels;
    Action<int> _modalMessageBoxOnClose;

    /// <summary>
    /// Displays a message which is modal with respect to this <r>Screen</r> -- in other words,
    /// while the message is displayed, this screen has a translucent "shield" over it, and the
    /// user cannot interact with this screen, but the user can still use other screens.
    /// </summary>
    ///
    /// <param name="message">The message to display, or <n>null</n> to clear the current message.
    ///     (<r>CloseModalMessageBox</r> can also be used instead of passing <n>null</n>.)</param>
    ///
    /// <remarks>
    /// <para>
    /// Only one modal message box can appear at a time.  The later one supercedes the earler one.
    /// </para>
    /// <para>
    /// This overload of this method does nothing if the specified message is already being
    /// displayed.
    /// </para>
    /// </remarks>
    ///
    public void ShowModalMessageBox(string message)
    {
        if ((message != _modalMessageBoxMessage) && (_modalMessageBoxButtonLabels == null) &&
            (_modalMessageBoxOnClose == null))
            ShowModalMessageBox(message, null, null);
    }

    public void ShowModalMessageBox(string message, string buttonLabels, Action<int> onClose)
    {
        CloseModalMessageBox();
        if (message != null)
        {
            _modalMessageBoxMessage = message;
            _modalMessageBoxButtonLabels = buttonLabels;
            _modalMessageBoxOnClose = onClose;
            if (Owner.CurrentScreen == this)
                ShowCurrentModalMessageBox();
        }
    }

    public bool HasModalMessageBox
    {
        get
        {
            return (_modalMessageBox != null);
        }
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public void CloseModalMessageBox()
    {
        if (_modalMessageBox != null)
        {
            ClientUtil.CloseDialog(_modalMessageBox, false);
            _modalMessageBox = null;
        }
        _modalMessageBoxMessage = null;
        _modalMessageBoxButtonLabels = null;
        _modalMessageBoxOnClose = null;
    }

    void ShowCurrentModalMessageBox()
    {
        if ((_modalMessageBoxMessage != null) && (_modalMessageBox == null))
        {
            _modalMessageBox = TouchMessageBox.Show(Control, _modalMessageBoxMessage,
                _modalMessageBoxButtonLabels,
                delegate(int buttonIndex)
                {
                    _modalMessageBox = null;
                    if (_modalMessageBoxOnClose != null)
                        _modalMessageBoxOnClose(buttonIndex);
                },
                new DialogSettings()
                {
                    IsModal = true
                });
        }
    }

    internal bool IsLoaded { get; set; }

    public event EventHandler Opened;
    public event EventHandler Closed;
    public event EventHandler Refreshed; // user clicked on tab of currently-open screen
    public event EventHandler Presented; // screen is opened, refreshed, or a dialog over it closed
    public event EventHandler Reset; // e.g. server disconnected -- remove any assumptions about server

    public bool IsOpen { get; private set; }

    internal void FireOpened()
    {
        IsOpen = true;
        if (Opened != null)
            Opened(this, EventArgs.Empty);
    }

    internal void FireClosed()
    {
        IsOpen = false;
        if (Closed != null)
            Closed(this, EventArgs.Empty);
    }

    internal void FireRefreshed()
    {
        if (Refreshed != null)
            Refreshed(this, EventArgs.Empty);
    }

    internal void FirePresented()
    {
        ShowCurrentModalMessageBox();
        if (Presented != null)
            Presented(this, EventArgs.Empty);
    }

    internal void FireReset()
    {
        if (Reset != null)
            Reset(this, EventArgs.Empty);
    }

    /// <summary>
    /// TODO
    /// </summary>
    ///
    public class InitializationArguments : ParsedArguments
    {
        Screen _screen;

        public InitializationArguments(Screen screen, string stringForm)
        {
            _screen = screen;
            ParseArguments(stringForm);
        }

        protected override Exception NewFormatException(string format, params object[] args)
        {
            return base.NewFormatException(
                "Missing or incorrect \"ScreenTab.InitArgs\" attribute in specification for screen \"{0}\" in ScreenChooser: {1}",
                _screen.Name, Util.Format(format, args));
        }
    }

    public void GoTo()
    {
        ClientGlobals.TryGoToScreen(Name);
    }
}

/// <summary>
/// Holds attached properties for use in children of the <r>TouchTabStrip</r> that identifies the
/// screens of the application.
/// </summary>
public static class ScreenTab
{
    /// <summary>
    /// Identifies the <n>ScreenControl</n> attached property, which can be used to specify an
	/// instance of a <n>Control</n> that implements the screen.  (If not provided, the <n>Tag</n>
	/// of the <n>ClientFramework.ScreenChooser</n> item is used as the name of the class that implements the
	/// screen.)
    /// </summary>
    ///
    public static readonly DependencyProperty ScreenControlProperty =
        DependencyProperty.RegisterAttached("ScreenControl", typeof(UserControl), typeof(ScreenTab),
            null);

    /// <summary>
    /// Identifies the <n>InitArgs</n> attached property, which specifies screen initialization
	/// arguments, in the form accepted by <r>ParsedArguments</r>.
    /// </summary>
    ///
    public static readonly DependencyProperty InitArgsProperty =
        DependencyProperty.RegisterAttached("InitArgs", typeof(string), typeof(ScreenTab),
			null);

    /// <summary>
    /// Identifies the <n>DefaultParentName</n> attached property, which specifies the name of the
	/// screen which is used as the default parent of the screen identified by the <n>TextBlock</n>
	/// (or other tab definition element within <r>TouchTabStrip</r>) that this property is
	/// attached to.
    /// </summary>
    ///
    public static readonly DependencyProperty DefaultParentNameProperty =
        DependencyProperty.RegisterAttached("DefaultParentName", typeof(string), typeof(ScreenTab),
			null);

    /// <summary>
    /// Identifies the <n>Label</n> attached property, which specifies a label for the
	/// <n>TextBlock</n> (or other tab definition within <r>TouchTabStrip</r>) that this property
	/// is attached to.  If <n>Label</n> is not specified, a label is typically inferred from the
	/// <n>TextBlock</n> text, or the element's <n>Tag</n> property.
    /// </summary>
    ///
    public static readonly DependencyProperty LabelProperty =
        DependencyProperty.RegisterAttached("Label", typeof(string), typeof(ScreenTab),
			null);

    /// <summary>
    /// Identifies the <n>DisableDirectNavigation</n> attached property, which specifies if
	/// navigation to a screen is disabled unless screen-specific parameters are set up in advance.
	/// So, for example, if <n>DisableDirectNavigation</n> is <n>true</n> for a screen, that screen
	/// will not appear in the list of screens that can be specified as the home screen.
    /// </summary>
    ///
    public static readonly DependencyProperty DisableDirectNavigationProperty =
        DependencyProperty.RegisterAttached("DisableDirectNavigation", typeof(bool),
			typeof(ScreenTab), null);

    /// <summary>
    /// Gets the value of the <n>ScreenTab.ScreenControl</n> property from a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element from which to get the property value.</param>
    ///
    public static UserControl GetScreenControl(FrameworkElement element)
    {
        return (UserControl)element.GetValue(ScreenControlProperty);
    }

    /// <summary>
    /// Sets the value of the <n>ScreenTab.ScreenControl</n> property on a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element on which to set the property value.</param>
    /// 
    /// <param name="value">The property value.</param>
    ///
    public static void SetScreenControl(FrameworkElement element, UserControl value)
    {
        element.SetValue(ScreenControlProperty, value);
    }

    /// <summary>
    /// Gets the value of the <n>ScreenTab.InitArgs</n> property from a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element from which to get the property value.</param>
    ///
    public static string GetInitArgs(FrameworkElement element)
    {
        return (string)element.GetValue(InitArgsProperty);
    }

    /// <summary>
    /// Sets the value of the <n>ScreenTab.InitArgs</n> property on a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element on which to set the property value.</param>
    /// 
    /// <param name="value">The property value.</param>
    ///
    public static void SetInitArgs(FrameworkElement element, string value)
    {
        element.SetValue(InitArgsProperty, value);
    }

    /// <summary>
    /// Gets the value of the <n>ScreenTab.DefaultParentName</n> property from a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element from which to get the property value.</param>
    ///
    public static string GetDefaultParentName(FrameworkElement element)
    {
        return (string)element.GetValue(DefaultParentNameProperty);
    }

    /// <summary>
    /// Sets the value of the <n>ScreenTab.DefaultParentName</n> property on a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element on which to set the property value.</param>
    /// 
    /// <param name="value">The property value.</param>
    ///
    public static void SetDefaultParentName(FrameworkElement element, string value)
    {
        element.SetValue(DefaultParentNameProperty, value);
    }

    /// <summary>
    /// Gets the value of the <n>ScreenTab.Label</n> property from a specified
    /// <n>FrameworkElement</n>.
	/// </summary>
    ///
    /// <param name="element">The element from which to get the property value.</param>
    ///
    public static string GetLabel(FrameworkElement element)
    {
        return (string)element.GetValue(LabelProperty);
    }

    /// <summary>
    /// Sets the value of the <n>ScreenTab.Label</n> property on a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element on which to set the property value.</param>
    /// 
    /// <param name="value">The property value.</param>
    ///
    public static void SetLabel(FrameworkElement element, string value)
    {
        element.SetValue(LabelProperty, value);
    }

    /// <summary>
    /// Gets the value of the <n>ScreenTab.DisableDirectNavigation</n> property from a specified
    /// <n>FrameworkElement</n>.
	/// </summary>
    ///
    /// <param name="element">The element from which to get the property value.</param>
    ///
    public static bool GetDisableDirectNavigation(FrameworkElement element)
    {
        return (bool)element.GetValue(DisableDirectNavigationProperty);
    }

    /// <summary>
    /// Sets the value of the <n>ScreenTab.DisableDirectNavigation</n> property on a specified
    /// <n>FrameworkElement</n>.
    /// </summary>
    ///
    /// <param name="element">The element on which to set the property value.</param>
    /// 
    /// <param name="value">The property value.</param>
    ///
    public static void SetDisableDirectNavigation(FrameworkElement element, bool value)
    {
        element.SetValue(DisableDirectNavigationProperty, value);
    }
}

/// <summary>
/// Specifies the whether or not <r>ClientFramework</r> is connected to the server, and similar
/// states.
/// </summary>
public enum ConnectionStatus
{
    /// <summary>
    /// <r>ClientFramework</r> is still initializing.
    /// </summary>
    Initializing,

    /// <summary>
    /// <r>ClientFramework</r> is attempting to connect to the server.
    /// </summary>
    AttemptingConnection,

    /// <summary>
    /// <r>ClientFramework</r> is connected to the server.
    /// </summary>
    Connected,

    /// <summary>
    /// ClientFramework.ClientEndpoint is missing some of the settings needed to connect to
    /// the server.
    /// </summary>
    ConnectionSettingsNeeded,

    /// <summary>
    /// The server reported that ClientFramework.ClientEndpoint.Password contains the wrong
    /// password.
    /// </summary>
    PasswordIsIncorrect,

    /// <summary>
    /// The connection to the server failed, and the user pressed the "Connection Settings" button.
    /// </summary>
    IntentionallyDisconnected,
}

public class SecondaryThreadException : Exception
{
    public SecondaryThreadException(Exception innerException) :
        base(String.Format("Exception in a secondary thread: {0}", innerException), innerException)
    {
    }
}

// the default implementation of ISyncClient
/// <summary>
/// TODO
/// </summary>
///
public class SyncClient : ISyncClient
{
    IMessageEndpoint _endpoint;

    internal Dictionary<Guid, Action<Guid, XmlReader>> _parcelRecipients =
        new Dictionary<Guid, Action<Guid, XmlReader>>();

    /// <summary>
    /// Registers a callback method to be called when this <r>ISyncClient</r> receives a
    /// parcel from the server that's destined for the caller.
    /// </summary>
    /// <param name="onParcelReceived">The callback method to call when a parcel is received
    ///     that's destined for the caller.</param>
    ///
    /// <returns>
    /// A "parcel receipient ID" (GUID) that the caller can use in messages it sends to the
    /// server.  Whenever the server sends a parcel to this client, and the parcel has a parcel
    /// recipient ID equal to this GUID, <pr>onParcelReceived</pr> will be called.
    /// </returns>
    public Guid RegisterParcelRecipient(Action<Guid, XmlReader> onParcelReceived)
    {
        Guid parcelRecipientId = Guid.NewGuid();
        _parcelRecipients[parcelRecipientId] = onParcelReceived;
        return parcelRecipientId;
    }

    /// <summary>
    /// Reverses what was done by a previous call to <r>RegisterParcelRecipient</r>.  Does nothing
    /// if the specified "parcel recipient ID" isn't found.
    /// </summary>
    ///
    /// <param name="recipientId">A "parcel recipient ID" (GUID) that was previously returned by
    ///     <r>RegisterParcelRecipient</r>.</param>
    ///
    public void UnregisterParcelRecipient(Guid recipientId)
    {
        _parcelRecipients.Remove(recipientId);
    }

    public IMessageEndpoint Endpoint
    {
        get
        {
            return _endpoint;
        }
        set
        {
            if (_endpoint != null)
            {
                _endpoint.IsConnectedChanged -= _endpoint_IsConnectedChanged;
                _endpoint.MessageReceived -= _endpoint_MessageReceived;
            }
            _endpoint = value;
            if (_endpoint != null)
            {
                _endpoint.IsConnectedChanged += _endpoint_IsConnectedChanged;
                _endpoint.MessageReceived += _endpoint_MessageReceived;
            }
        }
    }

    public IAlertsDisplay AlertsDisplay { get; set; }

    void _endpoint_IsConnectedChanged(object sender, EventArgs e)
    {
        // TODO: fire IsOnlineChanged?
        SetIsOnline(_endpoint.IsConnected);
    }

    /// <summary>
    /// The current instance ID of the server, or <n>null</n> if we haven't received an instance
    /// ID from the server yet.  The instance ID changes each time the server is started; if the
    /// client detects a change in the server instance ID the client will reload itself from the
    /// server.
    /// </summary>
    Guid _instanceId;

    static readonly SyncPath InstanceIdSyncPath = SyncPath.Build("InstanceId");

    void _endpoint_MessageReceived(object sender, MessageReceivedEventArgs e)
    {
        // retrieve event arguments
        var endpoint = (IMessageEndpoint)sender;
        Message message = e.Message;

        // if the message is named "MessageElements", its body consists of a sequence of "message
        // elements" (XML elements) -- process them
        if (message.Name == "MessageElements")
        {
            try
            {
                message.EnumerateMessageElements(delegate(XmlReader reader)
                {
                    try
                    {
                        SyncNode syncNode;
                        switch (reader.Name)
                        {

                        case "SyncNodeUpdate":

                            // this message element is a notification that a SyncNode changed; the
                            // change may have originated from this client, another client, or
                            // multiple clients (in the case of a merged update)
                            var update = SyncNodeUpdate.Deserialize(reader);

                            if (update.Path == InstanceIdSyncPath)
                            {
                                try
                                {
                                    Guid instanceId = XmlConvert.ToGuid(update.Value.ToString());
                                    if (_instanceId == Guid.Empty)
                                    {
                                        // this is the first instance ID we got from the server
                                        _instanceId = instanceId;
                                    }
                                    else
                                    if (_instanceId != instanceId)
                                    {
                                        // the instance ID changed, which means the server stopped
                                        // and restarted -- we need to reload this Silverlight
                                        // application from the server
                                        if (InstanceIdChanged != null)
                                            InstanceIdChanged(this, EventArgs.Empty);
                                    }
                                }
                                catch (FormatException)
                                {
                                }
                                //update.Value;
                            }

                            syncNode = (SyncNode)_syncTree.CreateNode(update.Path);
                            syncNode.OnServerReportedValueChange(update.Value, update.OriginatedHere);
                            break;

                        case "SyncNodeCommand":

                            // this message element is a notification that a SyncNodeCommand that
                            // originated from this client has completed
                            var command = SyncNodeCommand.Deserialize(reader);
                            syncNode = (SyncNode)_syncTree.CreateNode(command.Path);
                            syncNode.OnServerReportedOperationCompletion(command.OriginatedHere);
                            break;

                        case "Parcel":

                            using (XmlReader childReader = reader.ReadSubtree())
                            {
                                childReader.MoveToContent();
                                string recipientString = childReader.GetAttribute("Recipient");
                                Guid recipientId;
                                try
                                {
                                    recipientId = XmlConvert.ToGuid(recipientString);
                                }
                                catch (FormatException)
                                {
                                    // invalid GUID format -- ignore parcel
                                    break;
                                }
                                Action<Guid, XmlReader> onParcelReceived;
                                _parcelRecipients.TryGetValue(recipientId, out onParcelReceived);
                                if (onParcelReceived != null)
                                    onParcelReceived(recipientId, childReader);
                            }
                            break;

                        case "Alert":

                            string alertTypeString = reader.GetAttribute("Type");
                            if (alertTypeString == null)
                                throw new MessageProtocolException(null, "Missing Type attribute");
                            AlertType alertType;
                            switch (alertTypeString)
                            {
                                case "Information":
                                    alertType = AlertType.Information;
                                    break;
                                case "Warning":
                                    alertType = AlertType.Warning;
                                    break;
                                case null:
                                    throw new MessageProtocolException(null, "Missing Type attribute");
                                default:
                                    throw new MessageProtocolException(null,
                                        "Invald Type attribute value \"{0}\"", alertTypeString);
                            }
                            string alertMessage = reader.GetAttribute("Message");
                            if (alertMessage == null)
                                throw new MessageProtocolException(null, "Missing Message attribute");
                            AlertsDisplay.AddMessage(alertType, alertMessage);
                            break;
                        }
                    }
                    catch (MessageProtocolException)
                    {
                        // TODO: log (or at least trace): poorly formatted message elements
                    }
                });
            }
            catch (MessageProtocolException)
            {
                // TODO: log (or at least trace): e.g. invalid message XML
            }
        }
    }

    List<SyncNodeOperation> _sendToServer = new List<SyncNodeOperation>();

    /// <summary>
    /// The <r>SyncNode.DescendantChanged</r> events that need to be fired.  This collection is
    /// updated by <r>SyncNode.SetValue</r>, and the events are fired asynchronously (so that
    /// multiple events from the same event sender (<r>SyncNode</r>) can be collapsed into one).
    /// </summary>
    Dictionary<SyncNode, DescendantChangedEventArgs> _descendantsChangedQueue =
        new Dictionary<SyncNode, DescendantChangedEventArgs>();

    // see SyncNode for comments about how the SyncTree works
    SyncNode _syncTree;
    public SyncNodeBase SyncTree
    {
        get
        {
            return _syncTree;
        }
    }

    bool _isOnline;

    bool ISyncClient.IsOnline
    {
        get
        {
            return _isOnline;
        }
    }

    public void SetIsOnline(bool isOnline)
    {
        _isOnline = isOnline;
        if (IsOnlineChanged != null)
        {
            IsOnlineChanged(this, new IsOnlineChangedEventArgs()
            {
                IsOnline = isOnline
            });
        }
    }

    public event IsOnlineChangedEventHandler IsOnlineChanged;

    /// <summary>
    /// Fired when we detect that the server's instance ID changed, which means the server was
    /// stopped and restarted.  In that case, the Silverlight client application  needs to be
    /// reloaded from the server.
    /// </summary>
    public event EventHandler InstanceIdChanged;

    //    public bool IsSimulatedServerMode { get; set; }

    public SyncClient()
    {
        _syncTree = SyncNode.CreateTree(this);
    }

#if SILVERLIGHT
    static SyncClient _defaultSyncClient;

    public static void SetAsGlobalProvider()
    {
        if (_defaultSyncClient == null)
            _defaultSyncClient = new SyncClient();

        ClientGlobals.SyncClientProvider = delegate
        {
            return _defaultSyncClient;
        };
    }
#endif

    void ISyncClient.QueueSyncNodeToSendToServer(SyncNodeBase node)
    {
#if SILVERLIGHT
        SyncNodeUpdate update = new SyncNodeUpdate()
        {
            Path = node.Path,
            Value = node.Value
        };

        _sendToServer.Add(update);
        if (_sendToServer.Count == 1)
            Dispatcher.BeginInvoke(SendUpdatesToServer);
#else
        // TODO
#endif
    }

    void ISyncClient.QueueSyncNodeCommandToSendToServer(SyncNodeBase node, string verb)
    {
#if SILVERLIGHT
        SyncNodeCommand command = new SyncNodeCommand()
        {
            Path = node.Path,
            Verb = verb
        };

        _sendToServer.Add(command);
        if (_sendToServer.Count == 1)
            Dispatcher.BeginInvoke(SendUpdatesToServer);
#else
        // TODO
#endif
    }

    StringBuilder _builder = new StringBuilder(1000); // cached as an optimization

    void SendUpdatesToServer()
    {
        if (Endpoint != null) // i.e. if we're connected
        {
            _builder.Length = 0;
            using (XmlWriter writer = Message.CreateMessageElementWriter(_builder))
            {
                foreach (SyncNodeOperation operation in _sendToServer)
                    operation.Serialize(writer, false);
            }
            var message = new Message(_builder);
            Endpoint.SendMessage(message);
        }

        _sendToServer.Clear();

    }

#if SILVERLIGHT
    Dispatcher Dispatcher
    {
        get
        {
            return Application.Current.RootVisual.Dispatcher;
        }
    }
#endif

    void ISyncClient.QueueDescendantChangedEvent(SyncNodeBase syncNode, DescendantChanges changes, object originator)
    {
        DescendantChangedEventArgs eventArgs;
        if (!_descendantsChangedQueue.TryGetValue((SyncNode)syncNode, out eventArgs))
        {
            eventArgs = new DescendantChangedEventArgs()
            {
                Changes = changes,
                Originator = originator
            };
            _descendantsChangedQueue.Add((SyncNode)syncNode, eventArgs);
            if (_descendantsChangedQueue.Count == 1)
            {
                // this is the first item in the queue -- prepare to process the queue
                // asynchronously
#if SILVERLIGHT
                Application.Current.RootVisual.Dispatcher.BeginInvoke(delegate()
                {
                    foreach (KeyValuePair<SyncNode, DescendantChangedEventArgs> kvp in
                        _descendantsChangedQueue)
                    {
                        kvp.Key.FireDescendantChangedEvent(kvp.Key, kvp.Value, originator);
                    }
                    _descendantsChangedQueue.Clear();
                });
#endif
            }
        }
        else
        {
            eventArgs.Changes |= changes;
            if (eventArgs.Originator != originator)
                eventArgs.Originator = null; // event has multiple originators
        }
    }
}

}
